"""
    策略模板
"""
import datetime
import json

import numpy as np
import pandas as pd
import yaml
import os
from abc import ABC
from copy import copy
from typing import Any, Callable

from .constant import Interval, Direction, Offset, IntervalType
from BQ.entity.td_entity import BarData, TickData, OrderData, TradeData
from BQ.utils.utils import virtual
from .tools import StopOrder, EngineType, StopOrderStatus, Data
from BQ.entity.stra_entity import StraSignalEntity, StopOrderEntity, Bar, Tick
from event import EVENT_STRA_SIGNAL, EVENT_STRA, EVENT_STOP_ORDER


class TickBarMode:
    """策略的基础数据"""
    TICK = 0  # 仅使用
    BAR = 1  # 仅使用BAR，数据服务仅提供基于1－M的BAR，
    BOTH = 2  # 同时使用两种数据


class CtaTemplate(ABC):
    """
    策略模板

    """
    author = ""
    parameters = []
    variables = []
    # 本地委托ID列表
    long_order_ids = []
    short_order_ids = []
    order_ids = []

    def __init__(
            self,
            stra_engine: Any,
            symbol: str,
            setting: dict = None,
            periods: int = 0,
            strategy_name: str = '',
    ):
        """"""
        self.stra_engine = stra_engine
        self.strategy_name = strategy_name if strategy_name else '_'.join(
            [self.__class__.__name__, symbol, str(periods)])
        self.symbol = symbol
        self.periods = periods
        self.inited = False
        self.trading = False
        # 策略持仓、策略信号头寸、目标头寸：>0==多仓，<0==空仓
        self.pos = 0
        self.signal_pos = 0
        self.target_pos = 0

        # Copy a new variables list here to avoid duplicate insert when multiple
        # strategy instances are created with the same strategy class.
        self.variables = copy(self.variables)
        self.variables.insert(0, "inited")
        self.variables.insert(1, "trading")
        self.variables.insert(2, "pos")
        self.start_datetime = (datetime.datetime.now() - datetime.timedelta(days=-10)).strptime('%Y%m%d %H%M%S')

        if setting:
            self.update_setting(setting)

    def set_pos(self, pos):
        self.pos = pos

    def update_setting(self, setting: dict):
        """
        Update strategy parameter with value in setting dict.
        """
        for k, v in setting.items():
            setattr(self, k, v)

    def set_signal_pos(self, pos):
        """"""
        self.signal_pos = pos

    def get_signal_pos(self):
        """"""
        return self.signal_pos

    @classmethod
    def get_class_parameters(cls):
        """
        Get default parameters dict of strategy class.
        """
        class_parameters = {}
        for name in cls.parameters:
            class_parameters[name] = getattr(cls, name)
        return class_parameters

    def get_parameters(self):
        """
        Get strategy parameters dict.
        """
        strategy_parameters = {}
        for name in self.parameters:
            strategy_parameters[name] = getattr(self, name)
        return strategy_parameters

    def get_variables(self):
        """
        Get strategy variables dict.
        """
        strategy_variables = {}
        for name in self.variables:
            strategy_variables[name] = getattr(self, name)
        return strategy_variables

    def get_data(self):
        """
        Get strategy data.
        """
        strategy_data = {
            "strategy_name": self.strategy_name,
            "vt_symbol": self.symbol,
            "class_name": self.__class__.__name__,
            "author": self.author,
            "parameters": self.get_parameters(),
            "variables": self.get_variables(),
        }
        return strategy_data

    @virtual
    def on_init(self):
        """
        Callback when strategy is inited.
        调入相关数据，调用 on_ticks 或者 on_bars
        """
        pass

    @virtual
    def on_start(self):
        """
        Callback when strategy is started.
        要开始监听相应标的的行情数据 tick 及有关bar数据
        """
        pass

    @virtual
    def on_stop(self):
        """
        Callback when strategy is stopped.
        """
        pass

    @virtual
    def on_tick(self, tick: TickData):
        """
        Callback of new tick data update.
        """
        pass

    def on_ticks(self, ticks: pd.DataFrame):
        """
        Callback of ticks data when initing.
        """
        pass

    @virtual
    def on_bar(self, bar: BarData):
        """
        Callback of new bar data update.
        """
        pass

    def on_bars(self, bars: pd.DataFrame):
        """
        Callback of bars data when initting.
        """
        pass

    @virtual
    def on_trade(self, trade: TradeData):
        """
        Callback of new trade data update.
        """
        pass

    @virtual
    def on_order(self, order: OrderData):
        """
        Callback of new order data update.
        """
        pass

    @virtual
    def on_stop_order(self, stop_order: StopOrderEntity):
        """
        Callback of stop order update.
        """
        pass

    def check_stop_order(self, tick: TickData):
        """"""
        stop_order = self.stop_order

        long_triggered = (
                stop_order.direction == Direction.LONG and tick.last_price >= stop_order.price
        )
        short_triggered = (
                stop_order.direction == Direction.SHORT and tick.last_price <= stop_order.price
        )

        if long_triggered or short_triggered:
            strategy = self.strategies[stop_order.strategy_name]

            # To get excuted immediately after stop order is
            # triggered, use limit price if available, otherwise
            # use ask_price_5 or bid_price_5
            if stop_order.direction == Direction.LONG:
                if tick.limit_up:
                    price = tick.limit_up
                else:
                    price = tick.ask_price_5
            else:
                if tick.limit_down:
                    price = tick.limit_down
                else:
                    price = tick.bid_price_5

            contract = self.main_engine.get_contract(stop_order.symbol)

            vt_orderids = self.send_limit_order(
                strategy,
                contract,
                stop_order.direction,
                stop_order.offset,
                price,
                stop_order.volume,
                stop_order.lock
            )

            # Update stop order status if placed successfully
            if vt_orderids:
                # Remove from relation map.
                self.stop_orders.pop(stop_order.stop_orderid)

                strategy_vt_orderids = self.strategy_orderid_map[strategy.strategy_name]
                if stop_order.stop_orderid in strategy_vt_orderids:
                    strategy_vt_orderids.remove(stop_order.stop_orderid)

                # Change stop order status to cancelled and update to strategy.
                stop_order.status = StopOrderStatus.TRIGGERED
                stop_order.order_ids = vt_orderids

                self.call_strategy_func(
                    strategy, strategy.on_stop_order, stop_order
                )
                self.put_stop_order_event(stop_order)

    def buy(self, price: float, volume: float, stop: bool = False, lock: bool = False):
        """
        Send buy order to open a long position.
        """
        return self.send_order(Direction.LONG, Offset.OPEN, price, volume, stop, lock)

    def sell(self, price: float, volume: float, stop: bool = False, lock: bool = False):
        """
        Send sell order to close a long position.
        """
        return self.send_order(Direction.SHORT, Offset.CLOSE, price, volume, stop, lock)

    def short(self, price: float, volume: float, stop: bool = False, lock: bool = False):
        """
        Send short order to open as short position.
        """
        return self.send_order(Direction.SHORT, Offset.OPEN, price, volume, stop, lock)

    def cover(self, price: float, volume: float, stop: bool = False, lock: bool = False):
        """
        Send cover order to close a short position.
        """
        return self.send_order(Direction.LONG, Offset.CLOSE, price, volume, stop, lock)

    def send_order(
            self,
            direction: Direction,
            offset: Offset,
            price: float,
            volume: float,
            stop: bool = False,
            lock: bool = False
    ):
        """
        Send a new order.
        """
        if self.trading:
            order_id = self.stra_engine.send_order(
                self, direction, offset, price, volume, stop, lock
            )
            return order_id
        else:
            return []

    def cancel_order(self, order_id: str):
        """
        Cancel an existing order.
        """
        if self.trading:
            self.stra_engine.cancel_order(self, order_id)

    def cancel_all(self):
        """
        Cancel all orders sent by strategy.
        """
        if self.trading:
            self.stra_engine.cancel_all(self)

    def write_log(self, msg: str):
        """
        Write a log message.
        """
        self.stra_engine.write_log(msg, self)

    def get_engine_type(self):
        """
        Return whether the stra_engine is backtesting or live trading.
        """
        return self.stra_engine.get_engine_type()

    def get_pricetick(self):
        """
        Return pricetick data of trading contract.
        """
        return self.stra_engine.get_pricetick(self)

    def load_bar(
            self,
            days: int,
            interval: Interval = Interval.MINUTE,
            callback: Callable = None,
            use_database: bool = True
    ):
        """
        Load historical bar data for initializing strategy.
        """
        if not callback:
            callback = self.on_bar

        self.stra_engine.load_bar(
            self.symbol,
            days,
            interval,
            callback,
            use_database
        )

    def load_tick(self, days: int):
        """
        Load historical tick data for initializing strategy.
        """
        self.stra_engine.load_tick(self.symbol, days, self.on_tick)

    def put_event(self):
        """
        Put an strategy data event for ui update.
        """
        if self.inited:
            self.stra_engine.put_strategy_event(self)

    def send_email(self, msg):
        """
        Send email to default receiver.
        """
        if self.inited:
            self.stra_engine.send_email(msg, self)

    def sync_data(self):
        """
        Sync strategy variables value into disk storage.
        """
        if self.trading:
            self.stra_engine.sync_strategy_data(self)


class TargetPosTemplate(CtaTemplate):
    """"""
    tick_add = 1

    last_tick = None
    last_bar = None
    target_pos = 0

    def __init__(self, stra_engine, symbol, setting, periods: int = 0, strategy_name=''):
        """"""
        super().__init__(stra_engine, symbol, setting, periods=periods, strategy_name=strategy_name)

        self.active_orderids = []
        self.cancel_orderids = []

        self.variables.append("target_pos")

    # @virtual
    def on_tick(self, tick: TickData):
        """
        Callback of new tick data update.
        """
        self.last_tick = tick

        if self.trading:
            self.trade()

    # @virtual
    def on_bar(self, bar: BarData):
        """
        Callback of new bar data update.
        """
        self.last_bar = bar

    # @virtual
    def on_order(self, order: OrderData):
        """
        Callback of new order data update.
        """
        vt_orderid = order.vt_orderid

        if not order.is_active():
            if vt_orderid in self.active_orderids:
                self.active_orderids.remove(vt_orderid)

            if vt_orderid in self.cancel_orderids:
                self.cancel_orderids.remove(vt_orderid)

    def check_order_finished(self):
        """"""
        if self.active_orderids:
            return False
        else:
            return True

    def set_target_pos(self, target_pos):
        """"""
        self.target_pos = target_pos
        self.trade()

    def trade(self):
        """"""
        if not self.check_order_finished():
            self.cancel_old_order()
        else:
            self.send_new_order()

    def cancel_old_order(self):
        """"""
        for vt_orderid in self.active_orderids:
            if vt_orderid not in self.cancel_orderids:
                self.cancel_order(vt_orderid)
                self.cancel_orderids.append(vt_orderid)

    def send_new_order(self):
        """当产生新的信号时调用"""
        pos_change = self.target_pos - self.pos
        if not pos_change:
            return

        long_price = 0
        short_price = 0

        if self.last_tick:
            if pos_change > 0:
                long_price = self.last_tick.ask_price_1 + self.tick_add
                if self.last_tick.limit_up:
                    long_price = min(long_price, self.last_tick.limit_up)
            else:
                short_price = self.last_tick.bid_price_1 - self.tick_add
                if self.last_tick.limit_down:
                    short_price = max(short_price, self.last_tick.limit_down)

        else:
            if pos_change > 0:
                long_price = self.last_bar.close_price + self.tick_add
            else:
                short_price = self.last_bar.close_price - self.tick_add

        if self.get_engine_type() == EngineType.BACKTESTING:
            if pos_change > 0:
                vt_orderids = self.buy(long_price, abs(pos_change))
            else:
                vt_orderids = self.short(short_price, abs(pos_change))
            self.active_orderids.extend(vt_orderids)

        elif self.stra_engine.engine_type == EngineType.LIVE:
            if self.active_orderids:
                return

            if pos_change > 0:
                if self.pos < 0:
                    if pos_change < abs(self.pos):
                        vt_orderids = self.cover(long_price, pos_change)
                    else:
                        vt_orderids = self.cover(long_price, abs(self.pos))
                else:
                    vt_orderids = self.buy(long_price, abs(pos_change))
            else:
                if self.pos > 0:
                    if abs(pos_change) < self.pos:
                        vt_orderids = self.sell(short_price, abs(pos_change))
                    else:
                        vt_orderids = self.sell(short_price, abs(self.pos))
                else:
                    vt_orderids = self.short(short_price, abs(pos_change))
            self.active_orderids.extend(vt_orderids)


class StraTmpl(TargetPosTemplate):
    """基于多信号目标仓位的策略模板"""

    author = "Box Ro"

    # 定义参数
    period = 0
    p2 = '2'

    parameters = [
        "period",
        "p2",
    ]

    # 定义变量
    var1 = 1
    var2 = '2'
    bs_flag = 0  #
    posi_rate = 1  # 开仓比例
    variables = [
        "var1",
        "var2",
        "bssc_flag"

    ]
    # 本地委托ID列表
    long_order_ids = []
    short_order_ids = []
    order_ids = []

    tick_slip = 0  # 滑点

    last_tick = None
    last_bar = None

    def __init__(self, stra_engine, symbol, setting, periods=0, strategy_name=''):
        """"""
        super().__init__(stra_engine, symbol, setting, periods, strategy_name)
        self.load_vars()  # 调入前期保存的变量值
        self.load_params()  # 调入前期保存的参数值
        # 策略持仓、策略信号头寸、目标头寸：>0==多仓，<0==空仓
        self.pos = 0
        self.signal_pos = 0
        self.target_pos = 0
        # self.active_orderids = []  # 还未执行的委托
        # self.cancel_orderids = []
        self.setting = setting
        self.data_len = 0
        self.ps = stra_engine._rds.pubsub()

    def start(self):
        self.ps.psubscribe('*' + self.symbol)
        while True:
            for data in self.ps.listen():
                if data['type'] in ['message', 'pmessage']:
                    ed = json.loads(data['data'])
                    self.on_tick(ed)

    def load_params(self):
        """load params from saved yaml file"""
        fn = self.instance_name + '_params.yml'
        if not os.path.exists(fn):
            return

        with open(fn, 'r') as f:
            params = yaml.safe_load(f)
            for k, v in params.items():
                setattr(self, k, v)
            return params

    def save_params(self, params=None):
        """save params to yaml file"""
        fn = self.instance_name + '_params.yml'

        params = params if params else self.get_parameters()
        with open(fn, 'w') as f:
            yaml.safe_dump(params, f)

    def load_vars(self):
        fn = self.instance_name + '_vars.yml'
        if not os.path.exists(fn):
            return
        with open(fn, 'r') as f:
            vars = yaml.safe_load(f)
            for k, v in vars.items():
                setattr(self, k, v)
            return vars

    def save_vars(self, variables=None):
        """save vars to yaml file"""
        fn = self.instance_name + '_vars.yml'
        variables = variables if variables else self.get_variables()
        with open(fn, 'w') as f:
            yaml.safe_dump(variables, f)

    def close(self):
        self.save_vars()
        self.save_params()

    def send_signal(self, ocls, price, vol=1, position_rate=0.0, stop_price=0):
        """生成开平仓信号时调用"""
        signal = StraSignalEntity()
        signal.symbol = self.symbol
        signal.ocls = ocls
        signal.vol = vol
        signal.position_rate = position_rate
        signal.id = self.stra_name
        signal.price = price
        signal.datetime = datetime.datetime.now().strftime('%Y%m%d %H%M%S.%f')
        signal.variables = self.get_variables()
        signal.params = self.get_parameters()

        self.stra_engine.publish_signal(self.stra_name, signal)

    def send_stop_signal(self, price, ls=1):
        """
        开仓时设置止损价
        :param price: 开仓时设置的止损价格
        :param ls: long or short, 1==long, -1==short
        """
        so = StopOrderEntity()
        so.symbol = self.symbol
        so.price = price
        so.stra_name = self.stra_name
        so.ls = ls

        self.stra_engine.publish_stop_signal(self.stra_name, so)

        if ls == 1:
            self.set_long_stop_price(price)
        else:
            self.set_short_stop_price(price)

    def set_long_stop_price(self, price):
        """做多时设置止损价格"""
        pass

    def set_short_stop_price(self, price):
        """做空时设置止损价格"""
        pass

    @property
    def stra_name(self):
        return self.strategy_name

    @property
    def stra_instance_id(self):
        return '_'.join([self.__class__.__name__, self.symbol, str(self.period)])

    @property
    def instance_name(self):
        return self.stra_name if self.stra_name else '_'.join(
            [self.__class__.__name__, self.symbol, str(self.period)])

    @property
    def config(self):
        return self.setting

    @property
    def params(self):
        return self.parameters

    @property
    def vars(self):
        return self.variables

    @property
    def last_price(self):
        if self.last_tick:
            return self.last_tick.LastPrice

    lp = last_price


class StraTestTmpl(StraTmpl):
    def __init__(self, stra_engine, strategy_name, symbol, setting):
        super(StraTestTmpl, self).__init__(stra_engine, strategy_name, symbol, setting)
        self.pos = 0

    def send_signal(self, ocls, price, vol=1, position_rate=0.0, stop_price=0):
        """生成开平仓信号时调用"""
        signal = StraSignalEntity()
        signal.symbol = self.symbol
        signal.ocls = ocls
        signal.vol = vol
        signal.position_rate = position_rate
        signal.id = self.stra_name
        signal.price = price
        signal.datetime = datetime.datetime.now().strftime('%Y%m%d %H%M%S.%f')
        signal.variables = self.get_variables()
        signal.params = self.get_parameters()

        self.stra_engine.publish_signal(self.stra_name, signal)

    def send_stop_signal(self, price, ls=1):
        """
        开仓时设置止损价
        :param price: 开仓时设置的止损价格
        :param ls: long or short, 1==long, -1==short
        """
        so = StopOrderEntity()
        so.symbol = self.symbol
        so.price = price
        so.stra_name = self.stra_name
        so.ls = ls
        self.stra_engine.publish_stop_signal(self.stra_name, so)

        if ls == 1:
            self.set_long_stop_price(price)
        else:
            self.set_short_stop_price(price)

